<?php
/**
 * Class for managment of trees' stored in DB table 
 * with structure based on Nested Sets algorithm
 * 
 * @category   Zend
 * @package    Zend_Db
 * @author     Steshenko Alexander (http://lcf.name)
 * @license    http://framework.zend.com/license/new-bsd     New BSD License
 */
class Zend_Db_Tree_Nested
{
    /**
     * Tree table alias for external joining
     * TODO: stfalcon asked why do we need this, think about it.
     *
     */
    const TABLE_ALIAS = 'tree_nested_table_alias';
    
    /**
     * String keys used for internal representation
     * of nested sets structure table's columns
     */
    const ID        = 'id';
    const TREE_ID   = 'tree_id';
    const LEFT_KEY  = 'left_key';
    const RIGHT_KEY = 'right_key';
    const LEVEL     = 'level';
    
    /**
     * Return types
     */
    const RETURN_ARRAY  = 'array';
    const RETURN_OBJECT = 'Zend_Db_Select';
    
    /**
     * Fetch types
     */
    const FETCH_ALL = 'all';
    const FETCH_ROW = 'row'; 
    
    /**
     * String representation of default Nested Sets database adapter class.
     *
     * @var string
     */
    protected static $_defaultAdapterClass = 'Zend_Db_Tree_Nested_Adapter_Pdo_Mysql';
    
    /**
     * Nested Database Adapter. 
     * Note that it's not the same as database connection object.
     *
     * @var Zend_Db_Tree_Nested_Adapter_Interface
     */
    private $_adapter = null;
    
    /**
     * Return type for all returning data methods
     * By default it is Zend_Db_Select
     *
     * @var string
     */
    private $_returnType = self::RETURN_OBJECT;
    
    /**
     * Constructor.
     * 
     * If first param ($treeId) is configured database adapter
     * (instanse of Zend_Db_Tree_Nested_Adapter_Abstract) no other
     * params should be passed in constructor.

     * If first param ($treeId) is array with adapter configuration
     * no other params should be passed in constructor.
     *
     * @param integer|Zend_Db_Tree_Nested_Adapter_Abstract|array $treeId Object of 
     *	  adapter for nested sets object, config array for adapter or just tree id.
     * @param string $relativeTableName relative table name
     */
    public function __construct($treeId = null, $relativeTableName = null)
    {
        // setup adapter
        if ($treeId instanceof Zend_Db_Tree_Nested_Adapter_Abstract) {
            $this->_adapter = $treeId;
        } else {
            $adapterClass = self::getDefaultAdapterClass();
            try { // trying to load adapter for nested tree object
                Zend_Loader::loadClass($adapterClass);
            } catch (Exception $exception) { // class file not found
                throw new Zend_Db_Tree_Nested_Exception('Adapter class for nested'
                    . 'sets library: ' . $adapterClass . ' was not found. '
                    . 'Error message: ' . $exception->getMessage());
            }
            // $treeId can be config array for adapter
            if (is_array($treeId)) {
                $config = $treeId;
                $this->_adapter = new $adapterClass($config);
            } else {
                $this->_adapter = new $adapterClass();
                $this->_adapter->setTreeId($treeId);
                $this->_adapter->setRelativeTableName($relativeTableName);
            }
        }
    }
    
    /**
     * Returns id column for external joining
     *
     * @return string
     */
    public function getIdColumn()
    {
        return $this->_adapter->getIdColumn();
    }
        
    /**
     * Returns relative key column for external joining
     *
     * @return string
     */
    public function getRelativeKeyColumn()
    {
        return $this->_adapter->getRelativeKeyColumn();
    }
    
    /**
     * Sets the default class for nested sets' objects' adapters.
     *
     * @param string $adapter
     * @return void
     */
    public static function setDefaultAdapterClass($adapterClass)
    {
        self::$_defaultAdapterClass = $adapterClass;
    }

    /**
     * Gets the default class for nested sets' objects' adapters
     *
     * @return string
     */
    public static function getDefaultAdapterClass()
    {
        return self::$_defaultAdapterClass;
    }
    
    /**
     * Set return type for all data returning methods
     * Can be Zend_Db_Tree_Nested::RETURN_ARRAY or
     * Zend_Db_Tree_Nested::RETURN_OBJECT
     *
     * @param string $returnType
     */
    public function setReturnType($returnType)
    {
        if (($returnType != self::RETURN_ARRAY) && ($returnType != self::RETURN_OBJECT)) {
            throw new Zend_Db_Tree_Nested_Exception('Wrong value of'
                . ' return type: ' . $returnType);
        }
        $this->_returnType = $returnType;
    }
 
    /**
     * Define new tree id,
     * create root node in new tree
     * and returns new tree id.
     *
     * @return integer tree id
     */
    public function createTreeWithRootNode()
    {
        
        $treeId = $this->_adapter->getFreeTreeId(); // get free tree id
        $this->_adapter->setTreeId($treeId); // set new tree id to adapter
        $this->createChildNodeFirst();// create root node

        return $treeId;
    }
    
    /**
     * This method converts Zend_Db_Select Object into array, 
     * if return type is 'array'. Depends on fetchType the method uses
     * fetchRow or fetchArray method of adapter
     *
     * @param Zend_Db_Select $select
     * @param string $returnType
     * @param string $fetchType
     * @return array|Zend_Db_Select
     */
    protected function _formatDataWithReturnType($select, $returnType, $fetchType)
    {
        if ($returnType === null) { // if return type is not overwriten,
            $returnType = $this->_returnType; // get default value
        }
        if ($returnType == self::RETURN_ARRAY) {
            if ($fetchType == self::FETCH_ALL) {
                return $this->_adapter->fetchAll($select);
            } else {
                return $this->_adapter->fetchRow($select);
            }
        } else {
            return $select;
        }
    }
    
    /**
     * Get whole tree
     *
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getTree($returnType = null)
    {
        $select = $this->_adapter->getTree();
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ALL);
    }
    
    /**
     * This method temporary sets default columns
     * codes as aliases for internal needs of 
     * Zend_Db_Tree_Nested. 
     * It returns array with adapter aliases, which
     * can be set back into adapter by
     * $this->_adapter->setAliases();
     *
     * @return array
     */
    protected function _temporaryReplaceAliases()
    {
        $tmpAliases = $this->_adapter->getAliases();
        $this->_adapter->setAliases(array(
            self::ID => self::ID,
            self::TREE_ID => self::TREE_ID,
            self::LEFT_KEY => self::LEFT_KEY,
            self::RIGHT_KEY => self::RIGHT_KEY,
            self::LEVEL => self::LEVEL
        ));
        return $tmpAliases;
    }
    
    /**
     * For nested sets algorithm we often need
     * full information of the node. Its left, right keys,
     * level, id. So, undependant of aliases set in adapter
     * this method returns full information of the node
     * in array with constant columns codes as keys.
     *
     * @param integer $nodeId
     * @return array $node
     */
    protected function _getNodeFullInformation($nodeId)
    {
        // replace aliases for our query
        $tmpAliases = $this->_temporaryReplaceAliases();
        $node = $this->getNode($nodeId, self::RETURN_ARRAY);
        // restore aliases
        $this->_adapter->setAliases($tmpAliases);
        return $node;
    }
    
    /**
     * Get node information
     *
     * @param int $nodeId node's id
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getNode($nodeId, $returnType = null)
    {
        $select = $this->_adapter->getNode($nodeId);
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ROW);
    }
    
    /**
     * Getting root node's information. If there are several
     * root nodes - return first.
     * 
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getRootNode($returnType = null)
    {
        // First root node in the tree always has left key 1
        $select = $this->_adapter->getNodeByLeftKey(1);
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ROW);
    }
    
    /**
     * Get path to node (all nodes in node's branch)
     *
     * @param int $nodeId node's id
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getPath($nodeId, $returnType = null)
    {
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $select = $this->_adapter
                ->getPath($node[self::LEFT_KEY],
                          $node[self::RIGHT_KEY]);
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ALL);
    }
    
    /**
     * Get open tree, by active node id
     *
     * @param int $nodeId node's id
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getOpenTree($nodeId, $returnType = null)
    {
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $select = $this->_adapter
                ->getOpenTree($node[self::LEFT_KEY],
                              $node[self::RIGHT_KEY],
                              $node[self::LEVEL]);

        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ALL);
    }

    /**
     * Get all root nodes.
     *
     * @param string $returnType format of return data.
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getRootNodes($returnType = null)
    {
        $select = $this->_adapter->getRootNodes();
        
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ALL);
    }

    /**
     * Get parent node
     *
     * @param int $nodeId node's id
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getParent($nodeId, $returnType = null)
    {
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        if ($node[self::LEVEL] == 1) {
            // It's a root node - don't have a parent
            return false;
        } else {
            $select = $this->_adapter
                ->getParent(
                $node[self::LEFT_KEY],
                $node[self::RIGHT_KEY],
                $node[self::LEVEL]
                );
            return $this->_formatDataWithReturnType(
                $select, $returnType, self::FETCH_ROW);
        }
    }
    
    /**
     * Get children of the node. 
     * Note, that not all descendant will be selected, but only
     * children - descendant with level = parent level + 1
     *
     * @param int $nodeId node's id
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getChildren($nodeId, $returnType = null)
    {
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $select = $this->_adapter
                ->getChildren(
                    $node[self::LEFT_KEY],
                    $node[self::RIGHT_KEY],
                    $node[self::LEVEL]
                );
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ALL);
    }
    
    /**
     * Get all descendants of the node and the node itself (branch)
     *
     * @param int $nodeId node's id
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getBranch($nodeId, $returnType = null)
    {
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $select = $this->_adapter
                ->getBranch(
                    $node[self::LEFT_KEY],
                    $node[self::RIGHT_KEY]
                );
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ALL);
    }
    
    /**
     * Get all descendants of the node
     *
     * @param int $nodeId node's id
     * @param string $returnType format of return data. 
     *    Can be 'array' or 'Zend_Db_Select'.
     * @return Zend_Db_Select|array
     */
    public function getDescendants($nodeId, $returnType = null)
    {
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $select = $this->_adapter
                ->getDescendants(
                    $node[self::LEFT_KEY],
                    $node[self::RIGHT_KEY]
                );
        return $this->_formatDataWithReturnType($select, $returnType, self::FETCH_ALL);
    }
    
    /**
	 * Define is the node descendant of the another node or it is not.
	 * Returns true or false
     *
     * @param int $descendantId descendant node id
     * @param int $parentId parent node id
     * @return boolean
     */
    public function isDescendant($descendantId, $parentId)
    {
        $this->_adapter->beginTransaction();
                
        $parent = $this->_getNodeFullInformation($parentId);
        if (!$parent) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $parentId . ' was not found ');
        }
        $descendant = $this->getNode($descendantId, self::RETURN_ARRAY);
        
        $this->_adapter->commitTransaction();
        
        if (!$descendant) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $descendantId . ' was not found ');
        }

        $descendantLeftKey = $descendant[self::LEFT_KEY];
        $parentRightKey    = $parent[self::RIGHT_KEY];
        $parentLeftKey     = $parent[self::LEFT_KEY];
        
        if (($descendantLeftKey < $parentRightKey) 
            && ($descendantLeftKey > $parentLeftKey)) {
            // left key of descendant is between parents keys
            return true;
        } else {
            return false;
        }
    }
    
    /**
     * Check has the node children or it has not.
 	 * Returns true or false
 	 * 
     * @param int $nodeId node id
     * @return boolean
     */
    public function hasChildrens($nodeId)
    {
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $leftKey = $node[self::LEFT_KEY];
        $rightKey = $node[self::RIGHT_KEY];
        if (($rightKey - $leftKey) > 1) {
            return true;
        } else {
            return false;
        }
    }
    
    /**
     * Create child node and put it first on the level
     *
     * @param int $parentId parent node id
     * @return int new node id
     */
    public function createChildNodeFirst($parentId = 0)
    {
        $this->_adapter->beginTransaction();
        
        if ($parentId == 0) {    // new root node
            // Get minimal left key
            $minLeftKey = $this->_adapter->getMinLeftKey();
            // prepare data
            $leftKey = $minLeftKey - 1;
            $level   = 0;
        } else {
            $node    = $this->_getNodeFullInformation($parentId);
            $leftKey = $node[self::LEFT_KEY];
            $level   = $node[self::LEVEL];
        }
        
        // update tree
        $this->_adapter->createUpdate($leftKey);
        // insert new node and obtain its id
        $id = $this->_adapter->insertNode($leftKey + 1, $leftKey + 2, $level + 1);

        $this->_adapter->commitTransaction();
        
        return $id;
    }
    
    /**
     * Create child node and put it last on the level
     *
     * @param int $parentId parent node id
     * @return int new node id
     */
    public function createChildNodeLast($parentId = 0)
    {
        // Начало транзакции
        $this->_adapter->beginTransaction();
        
        if ($parentId == 0) {    // new root node
            // get maximal right key in the tree
            $maxRightKey = $this->_adapter->getMaxRightKey();
            // prepare data
            $rightKey = $maxRightKey + 1;
            $level    = 0;
        } else {
            $node     = $this->_getNodeFullInformation($parentId);
            $rightKey = $node[self::RIGHT_KEY];
            $level    = $node[self::LEVEL];
        }    

        // update tree
        $this->_adapter->createUpdate($rightKey);
        // insert new node and obtain its id
        $id = $this->_adapter->insertNode($rightKey, $rightKey + 1, $level + 1);

        $this->_adapter->commitTransaction();
        
        return $id;
    }
    
    /**
     * Create new adjacent node,
     * put it to next on the level 
     *
     * @param int $node node id
     * @return int new node id
     */
    public function createAdjacentNodeNext($nodeId)
    {
        $this->_adapter->beginTransaction();
        
        $node     = $this->_getNodeFullInformation($nodeId);
        $rightKey = $node[self::RIGHT_KEY];
        $level    = $node[self::LEVEL];
        
        // update the tree
        $this->_adapter->createUpdate($rightKey, true);
        // insert new node and obtain its id
        $id = $this->_adapter->insertNode($rightKey + 1, $rightKey + 2, $level);

        $this->_adapter->commitTransaction();
        
        return $id;
    }
    
    /**
     * Create new adjacent node,
     * put it to previous on the level 
     *
     * @param int $node node id
     * @return int new node id
     */
    public function createAdjacentNodePrevious($nodeId)
    {
        $this->_adapter->beginTransaction();
        
        $node     = $this->_getNodeFullInformation($nodeId);
        $leftKey  = $node[self::LEFT_KEY];
        $level    = $node[self::LEVEL];

        // update the tree
        $this->_adapter->createUpdate($leftKey, true);
        // insert new node and obtain its id
        $id = $this->_adapter->insertNode($leftKey, $leftKey + 1, $level);

        $this->_adapter->commitTransaction();
        
        return $id;
    }

    /**
     * Move node
     *
     * @param int $leftKey moved node left key
     * @param int $rightKey moved node right key
     * @param int $level moved node level
     * @param int $levelNew new level
     * @param int $near point of movement. New left key of movemend node - 1
     * @return boolean
     */
    protected function _moveNode($leftKey, $rightKey, $level, $levelNew, $near)
    {
         if (($near >= $leftKey) && ($near <= $rightKey)) {
            // Check on the self-to-self movement
            return false;
        }
        // Determine the moved node's keys' offset
        $skewTree = $rightKey - $leftKey + 1;
        // Level's offset
        $skewLevel = $levelNew - $level;

        if ($rightKey < $near) { // If it is move up
            return $this->_adapter->moveNodeUp($leftKey, $rightKey, $skewTree,
                 $skewLevel, $near);
        } else {    // if it is move down
            return $this->_adapter->moveNodeDown($leftKey, $rightKey, $skewTree,
                 $skewLevel, $near);
        }
    }
    
    /**
     * Move node to the previous node's place. Changes their places.
     *
     * @param int $nodeId node id
     * @return boolean was movement succesful or not
     */
    public function moveToPrevious($nodeId)
    {
        $this->_adapter->beginTransaction();
        
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $rightKey = $node[self::RIGHT_KEY];
        $leftKey  = $node[self::LEFT_KEY];
        $level    = $node[self::LEVEL];
        // calculate $near param. For this, replace aliases temporary
        $tmpAliases = $this->_temporaryReplaceAliases();
        $nearNode = $this->_adapter->fetchRow( 
            $this->_adapter->getNodeByRightKey($leftKey - 1));
        $this->_adapter->setAliases($tmpAliases);
        if (!$nearNode) {
            // there is no place to move
            return false;
        } 
        $near = $nearNode[self::LEFT_KEY] - 1;
        $result = $this->_moveNode($leftKey, $rightKey, $level, $level, $near);

        $this->_adapter->commitTransaction();
        
        return $result;
    }

    /**
     * Move node to the next node's place. Changes their places.
     *
     * @param int $nodeId node id
     * @return boolean was movement succesful or not
     */
    public function moveToNext($nodeId)
    {
        $this->_adapter->beginTransaction();
        
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $rightKey = $node[self::RIGHT_KEY];
        $leftKey  = $node[self::LEFT_KEY];
        $level    = $node[self::LEVEL];
        // calculate $near param
        $tmpAliases = $this->_temporaryReplaceAliases();
        $nearNode = $this->_adapter->fetchRow(
            $this->_adapter->getNodeByLeftKey($rightKey + 1));
        $this->_adapter->setAliases($tmpAliases);
        if (!$nearNode) {
            // there is no place to move
            return false;
        } 
        $near = $nearNode[self::RIGHT_KEY];
        $result = $this->_moveNode($leftKey, $rightKey, $level, $level, $near);
        
        $this->_adapter->commitTransaction();
        
        return $result;
    }
    
	/**
     * Moves one node to another node and put it before on the same level
     *
     * @param int $nodeId node id to move
     * @param int $moveToNodeId target node
     * @return boolean was movement succesful or not
     */
    public function moveToNodePrevious($nodeId, $moveToNodeId)
    {

        $this->_adapter->beginTransaction();
        
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        // obtain target node's params
        $moveToNode = $this->_getNodeFullInformation($moveToNodeId);
        if (!$moveToNode) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $moveToNodeId . ' was not found ');
        }
        $rightKey = $node[self::RIGHT_KEY];
        $leftKey  = $node[self::LEFT_KEY];
        $level    = $node[self::LEVEL];
        $moveToNodeLeftKey  = $moveToNode[self::LEFT_KEY];
        $moveToNodeLevel    = $moveToNode[self::LEVEL];
        
        $result = $this->_moveNode($leftKey, $rightKey, $level, $moveToNodeLevel, --$moveToNodeLeftKey);
        
        $this->_adapter->commitTransaction();
        
        return $result;
    }
    
    /**
     * Moves one node to another node and put it after on the same level
     *
     * @param int $nodeId node id to move
     * @param int $moveToNodeId target node
     * @return boolean was movement succesful or not
     */
    public function moveToNodeNext($nodeId, $moveToNodeId)
    {
        $this->_adapter->beginTransaction();
        
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        // obtain target node's params
        $moveToNode = $this->_getNodeFullInformation($moveToNodeId);
        if (!$moveToNode) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $moveToNodeId . ' was not found ');
        }
        $rightKey = $node[self::RIGHT_KEY];
        $leftKey  = $node[self::LEFT_KEY];
        $level    = $node[self::LEVEL];
        $moveToNodeRightKey = $moveToNode[self::RIGHT_KEY];
        $moveToNodeLevel    = $moveToNode[self::LEVEL];

        $result = $this->_moveNode($leftKey, $rightKey, $level, $moveToNodeLevel, $moveToNodeRightKey);
        
        $this->_adapter->commitTransaction();
        
        return $result;
    }
    
    /**
     * Move one node to another as child, first in the child's list.
     * Returns true or false
     * 
     * @param int $nodeId node's id
     * @param int $parentId parent node's id
     * @return boolean
     */
    public function moveToChildrenFirst($nodeId, $parentId)
    {
        $this->_adapter->beginTransaction();
        
        // Obtain data for moving node
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $rightKey = $node[self::RIGHT_KEY];
        $leftKey  = $node[self::LEFT_KEY];
        $level    = $node[self::LEVEL];

        if ($parentId == 0) { // Movement to root
            $near = 0;
            $levelNew = 1;
        } else {
            // obtain target parent node's data
            $parent = $this->_getNodeFullInformation($parentId);
            if (!$parent) {
                throw new Zend_Db_Tree_Nested_Exception('Node with id '
                    . $parentId . ' was not found ');
            }
            $parentLeftKey  = $parent[self::LEFT_KEY];
            $parentLevel    = $parent[self::LEVEL];
            
            $near = $parentLeftKey;
            $levelNew = $parentLevel + 1;
        }
        
        $result = $this->_moveNode($leftKey, $rightKey, $level, $levelNew, $near);

        $this->_adapter->commitTransaction();

        return $result;
    }
    
    /**
     * Move one node to another as child, last in the child's list.
     * Returns true or false
     * 
     * @param int $nodeId node's id
     * @param int $parentId parent node's id
     * @return boolean
     */
    public function moveToChildrenLast($nodeId, $parentId)
    {
        $this->_adapter->beginTransaction();
        
        // Obtain data for moving node
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $rightKey = $node[self::RIGHT_KEY];
        $leftKey  = $node[self::LEFT_KEY];
        $level    = $node[self::LEVEL];

        if ($parentId == 0) { // Movement to root
            $maxRightKey = $this->_adapter->getMaxRightKey();
            $near = $maxRightKey;
            $levelNew = 1;
        } else {
            // obtain target parent node's data
            $parent = $this->_getNodeFullInformation($parentId);
            if (!$parent) {
                throw new Zend_Db_Tree_Nested_Exception('Node with id '
                    . $parentId . ' was not found ');
            }
            $parentRightKey  = $parent[self::RIGHT_KEY];
            $parentLevel    = $parent[self::LEVEL];
            
            $near = $parentRightKey - 1;
            $levelNew = $parentLevel + 1;
        }
        
        $result = $this->_moveNode($leftKey, $rightKey, $level, $levelNew, $near);

        $this->_adapter->commitTransaction();
        
        return $result;
    }
    
    /**
     * Delete node and all descendants
     *
     * @param int $nodeId node's id
     * @return boolean
     */
    public function delete($nodeId)
    {
        $this->_adapter->beginTransaction();
        
        $node = $this->_getNodeFullInformation($nodeId);
        if (!$node) {
            throw new Zend_Db_Tree_Nested_Exception('Node with id '
                . $nodeId . ' was not found ');
        }
        $rightKey = $node[self::RIGHT_KEY];
        $leftKey  = $node[self::LEFT_KEY];

        // get all deletend node's ids. for delete data from relate table
        $tmpAliases = $this->_temporaryReplaceAliases();
        $deletedNodes = $this->getDescendants($nodeId, self::RETURN_ARRAY);
        $this->_adapter->setAliases($tmpAliases);
        
        $deletedIds = array($nodeId);
        foreach ($deletedNodes as $deletedNode) {
            $deletedIds[] = $deletedNode[self::ID];
        }
        // delete node and descendant
        $this->_adapter->delete($leftKey, $rightKey);
        // delete data from relative table if defined
        $this->_adapter->deleteCallback($deletedIds);
        // update the tree
        $this->_adapter->deleteUpdate($leftKey, $rightKey);

        $this->_adapter->commitTransaction();
        
        return true;
    }
    
    /**
     * Delete whole tree
     *
     * @return boolean
     */
    public function deleteTree()
    {
        $this->_adapter->beginTransaction();

        // get all deletend node's ids. for delete data from relate table
        $tmpAliases = $this->_temporaryReplaceAliases();
        $deletedNodes = $this->getTree(self::RETURN_ARRAY);
        $this->_adapter->setAliases($tmpAliases);
        $deletedIds = array();
        foreach ($deletedNodes as $deletedNode) {
            $deletedIds[] = $deletedNode[self::ID];
        }
        
        // delete the tree
        $this->_adapter->deleteTree();
        // delete data from relative tables if defined
        $this->_adapter->deleteCallback($deletedIds);

        $this->_adapter->commitTransaction();
        
        return true;
    }
    
    /**
     * Tree Validation
     * Returns true or false
     *
     * @return boolean
     */
    public function validateTree()
    {
        $check = $this->_adapter->validateLeftRightKeys();
        $check &= $this->_adapter->validateMaxMinKeys();
        $check &= $this->_adapter->validateKeysDiff();
        $check &= $this->_adapter->validateUniqKeys();

        return (boolean) $check;
    }
}